from abc import ABC, abstractmethod
from datetime import datetime, time
import pandas as pd
from importlib import resources
import time as ptime
from .vlogger import VLogger
from typing import List

# gm imports
from gm.api import schedule, get_next_trading_date, order_target_value as jj_order_target_value, history, \
    order_target_percent as jj_order_target_percent, order_volume as jj_order_lots, get_unfinished_orders, \
    OrderType_Limit, PositionSide_Long, PositionEffect_Open, \
    OrderStatus_New, OrderStatus_PartiallyFilled, OrderStatus_PendingNew, OrderStatus_PendingCancel, \
    get_instruments as jj_get_instruments, current, ADJUST_NONE, MODE_BACKTEST, OrderType_Market
# rqalpha imports
from rqalpha.apis.api_abstract import order_target_percent as rq_order_target_percent, \
    order_target_value as rq_order_target_value
from rqalpha.apis.api_base import all_instruments as rq_all_instruments, \
    get_next_trading_date as rq_get_next_trading_date, subscribe as rq_subscribe, \
    get_positions as rq_get_positions, subscribe_event as rq_subscribe_event
from rqalpha.const import ORDER_STATUS
from rqalpha.core.events import EVENT
from rqalpha.mod.rqalpha_mod_sys_accounts.api.api_stock import is_suspended as rq_is_suspended, \
    order_lots as rq_order_lots
from rqalpha.mod.rqalpha_mod_sys_scheduler.scheduler import Scheduler

# futu imports


class Trader(ABC):

    def __init__(self):
        self._context = None

    @abstractmethod
    def is_suspended(self, order_book_id: str, dt: datetime):
        pass

    @abstractmethod
    def flush_open_orders(self):
        pass

    @abstractmethod
    def order_target_percent(self, order_book_id: str, target_percent: float,
                             price: float):
        pass

    @abstractmethod
    def order_target_value(self, order_book_id: str, target_value: float,
                           price: float):
        pass

    @abstractmethod
    def order_lots(self, order_book_id: str, lots: int, price: float):
        pass

    @abstractmethod
    def all_instruments(self, ins_type: str, dt: str):
        pass

    @abstractmethod
    def get_next_trading_date(self, datestr: str, n: int):
        pass

    @abstractmethod
    def run_weekly(self, handle_bar_fn, tradingday: int):
        pass

    @abstractmethod
    def run_daily(self, handle_bar_fn):
        pass

    @abstractmethod
    def run_monthly(self, handle_bar_fn, tradingday: int):
        pass

    @abstractmethod
    def subscribe(self, order_book_id: str):
        pass

    @abstractmethod
    def get_positions(self) -> List:
        pass

    def set_context(self, context):
        self._context = context

    @staticmethod
    def trading_date_in_n(datestr: str, n: int):
        with resources.path(__package__, 'trading_dates.csv') as f:
            td = pd.read_csv(f).trading_date.tolist()
        try:
            idx = td.index(datestr)
            return td[idx + n]
        except ValueError:
            import bisect
            if n >= 0:
                idx = bisect.bisect(td, datestr) - 1
                return td[idx + n]
            else:
                idx = bisect.bisect_right(td, datestr)
                return td[idx + n]


class RqTrader(Trader):

    def __init__(self, freq: str):
        super().__init__()
        self._freq = freq

    def is_suspended(self, order_book_id: str, _) -> bool:
        return rq_is_suspended(order_book_id)

    def flush_open_orders(self):
        pass

    def order_target_percent(self, order_book_id: str, target_percent: float,
                             price: float):
        return rq_order_target_percent(order_book_id, target_percent, price)

    def order_target_value(self, order_book_id: str, target_value: float,
                           price: float):
        return rq_order_target_value(order_book_id, target_value, price)

    def order_lots(self, order_book_id: str, lots: int, price: float):
        return rq_order_lots(order_book_id, lots, price)

    def get_next_trading_date(self, datestr: str, n: int):
        return rq_get_next_trading_date(datestr, n)

    def all_instruments(self, ins_type: str, dt: str):
        return rq_all_instruments(type=ins_type, date=dt)

    def run_weekly(self, handle_bar_fn, tradingday: int):
        Scheduler(self._freq).run_weekly(handle_bar_fn, tradingday)

    def run_daily(self, handle_bar_fn):
        Scheduler(self._freq).run_daily(handle_bar_fn)

    def run_monthly(self, handle_bar_fn, tradingday: int):
        Scheduler(self._freq).run_monthly(handle_bar_fn, tradingday)

    def subscribe(self, order_book_id: str):
        rq_subscribe(order_book_id)
        rq_subscribe_event(EVENT.TRADE, self.on_order_event)

    def get_positions(self):
        return [p.order_book_id for p in rq_get_positions()]

    def on_order_event(self, context, event):
        dt = context.now
        order = event.order
        if order is None:
            print('{}: Order create error: {}'.format(dt, order))
        elif order.status != ORDER_STATUS.FILLED:
            print('{}: Order error: {}'.format(dt, order))


class JuejinTrader(Trader):

    def __init__(self, mode):
        super().__init__()
        self._mode = mode

    def flush_open_orders(self, sleep_sec=10):
        VLogger.vlog(0, 'Flushing open orders')
        while True:
            flushed = True
            # It returns some rejected order as unfinished, need to filter them out
            for order in get_unfinished_orders():
                if order.status in [
                        OrderStatus_New, OrderStatus_PartiallyFilled,
                        OrderStatus_PendingNew, OrderStatus_PendingCancel
                ]:
                    flushed = False
                    VLogger.vlog(0, 'Inflight order: %s' % order)
            if flushed:
                break
            else:
                VLogger.vlog(
                    0, 'Waiting for orders to close, sleep for %s seconds...' %
                    sleep_sec)
                ptime.sleep(sleep_sec)

    def get_positions(self):
        return [p.symbol for p in self._context.account().positions()]

    def is_suspended(self, order_book_id: str, dt: datetime) -> bool:
        df = history(symbol=order_book_id,
                     frequency='60s',
                     start_time=dt.date(),
                     end_time=dt.date(),
                     adjust=ADJUST_NONE,
                     df=True)
        return df.empty

    def order_target_percent(self, order_book_id: str, target_percent: float,
                             price: float):
        if self._mode == MODE_BACKTEST:
            target_volume = self._context.account().cash.nav * target_percent / price
            p = self._context.account().position(order_book_id, PositionSide_Long)
            delta = target_volume
            if p is not None:
                delta = target_volume - p.volume
            if abs(delta) < 10:
                VLogger.error('Invalid volume: %s' % delta)
            else:
                order = jj_order_target_percent(symbol=order_book_id,
                                                percent=target_percent,
                                                order_type=OrderType_Market,
                                                position_side=PositionSide_Long)
                if order[0].volume < 10:
                    VLogger.error('%s %s %s %s %s' % (delta, p, self._context.account().cash.nav, target_percent, price))
                    raise Exception('fatal')
                return order
        else:
            data = current(symbols=order_book_id)
            VLogger.vlog(0, data[0].quotes)
            if target_percent == 0.0:
                VLogger.vlog(
                    0, 'Closing %s at %s' %
                    (order_book_id, data[0].quotes[2].bid_p))
                return jj_order_target_percent(symbol=order_book_id,
                                               percent=target_percent,
                                               order_type=OrderType_Limit,
                                               position_side=PositionSide_Long,
                                               price=data[0].quotes[2].bid_p)
            else:
                VLogger.vlog(
                    0, 'Rebalancing %s at %s' %
                    (order_book_id, data[0].quotes[2].ask_p))
                return jj_order_target_percent(symbol=order_book_id,
                                               percent=target_percent,
                                               order_type=OrderType_Limit,
                                               position_side=PositionSide_Long,
                                               price=data[0].quotes[2].ask_p)

    def order_target_value(self, order_book_id: str, target_value: float,
                           price: float):
        return jj_order_target_value(symbol=order_book_id,
                                     value=target_value,
                                     order_type=OrderType_Limit,
                                     position_side=PositionSide_Long,
                                     price=price)

    def order_lots(self, order_book_id: str, lots: int, price: float):
        return jj_order_lots(symbol=order_book_id,
                             volume=lots,
                             price=price,
                             order_type=OrderType_Limit,
                             position_effect=PositionEffect_Open,
                             side=PositionSide_Long)

    def all_instruments(self, ins_type: str, dt: str):
        return jj_get_instruments(sec_types=ins_type, df=True)

    def get_next_trading_date(self, datestr: str, n: int) -> str:
        for i in range(n):
            datestr = get_next_trading_date('SZSE', datestr)
        return str(datestr)

    def run_weekly(self, handle_bar_fn, tradingday: int):
        schedule(schedule_func=handle_bar_fn,
                 date_rule='1w',
                 time_rule='09:35:00')

    def run_daily(self, handle_bar_fn):
        schedule(schedule_func=handle_bar_fn,
                 date_rule='1d',
                 time_rule='09:35:00')

    def run_monthly(self, handle_bar_fn, tradingday: int):
        schedule(schedule_func=handle_bar_fn,
                 date_rule='1m',
                 time_rule='09:35:00')

    def subscribe(self, order_book_id: str):
        pass


class FutuTrader(Trader):

    def flush_open_orders(self):
        pass

    def __init__(self):
        super().__init__()

    def get_positions(self):
        pass

    def is_suspended(self, order_book_id: str, dt: datetime) -> bool:
        return False

    def order_target_percent(self, order_book_id: str, target_percent: float,
                             price: float):
        raise Exception('Unimplemented')

    def order_target_value(self, order_book_id: str, target_value: float,
                           price: float):
        raise Exception('Unimplemented')

    def order_lots(self, order_book_id: str, lots: int, price: float):
        pass

    def all_instruments(self, ins_type: str, dt: str):
        raise Exception('Unimplemented')

    def get_next_trading_date(self, datestr: str, n: int):
        raise Exception('Unimplemented')

    def run_weekly(self, handle_bar_fn, tradingday: int):
        raise Exception('Unimplemented')

    def run_daily(self, handle_bar_fn):
        raise Exception('Unimplemented')

    def run_monthly(self, handle_bar_fn, tradingday: int):
        raise Exception('Unimplemented')

    def subscribe(self, order_book_id: str):
        pass
